Android 性能优化: 启动优化理论与实践
The following article is from 小余的自习室 Author 小余的自习室
本文章总结了目前市面上常见的一些启动优化常用手段,开发和面试必备哦。首先要做应用启动优化,你得对应用启动流程有个整体甚至细化的了解。
Application启动阶段
Activity启动阶段
这里经过对应用启动全流程分析,提取出了应用的一些可触及点,这些可触及点就是我们启动优化过程中应用可优化部分。
1.CPU time:指不合理的占用CPU时间片。举例:
一般方案:
UI构建阶段:
2.使用RenderThread线程将1中绘制好的数据提供给GPU去渲染,这里涉及到进程间通讯。
工具:TraceView、Systrace、Android Profiler ,抖音Rhea
Systrace
Trace.beginSection("MyApp.onCreate_1");
alt(200);
Trace.endSection();
TraceView
Android Profiler
studio自带的性能分析利器。不仅可以分析当前应用的CPU使用率,还可以记录当前应用的内存使用方式。
可以直接替代TraceView记录方法耗时信息。
Debug.startMethodTracing();
back(100);
alt(200);
Debug.stopMethodTracing();
Rhea
地址:https://juejin.cn/post/7052625610295738382
4.1:主线程直接优化
4.1.1:4.x机型存在MutilDex问题,
4.1.2:ContentProvider优化
FileProvider 是 Android7.0 引入的用于进行文件访问权限控制的组件:
Uri.fromFile(new File(filePath));
FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider",new File(filePath));
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
super.attachInfo(context, info);
// Check our security attributes
if (info.exported) {
throw new SecurityException("Provider must not be exported");
}
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grant uri permissions");
}
mStrategy = getPathStrategy(context, info.authority.split(";")[0]);
}
进入getPathStrategy:
private static PathStrategy getPathStrategy(Context context, String authority) {
PathStrategy strat;
synchronized (sCache) {
strat = sCache.get(authority);
if (strat == null) {
try {
strat = parsePathStrategy(context, authority);
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
} catch (XmlPullParserException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
}
sCache.put(authority, strat);
}
}
return strat;
}
为了优化这部分,我们可以使用字节码插桩的方式:
4.1.3:Application 的 onCreate 阶段优化
- 1.Application 中的任务应当是全局核心任务,就是一定要这个时候执行的
- 2.Application 创建时应当尽量减少网络请求操作,网络请求会调用IO线程执行下载,会占用较多CPU时间片信息。
- 3.Application 创建时不允许有强业务相关的任务
- 4.Application 创建时尽量减少有 Json 解析处理和 IO 操作的工作
基础库初始化任务:主要是对网络库,日志库等基础库进行初始化配置,我们最终目标是在启动阶段删除这些任务。首先这些任务会有些耗时,删除了又可能会影响功能的稳定性。 主要优化方式:对任务进行原子化改造,对于需要向sdk中注入context,callback等各类参数的实现,改为按需调用。 功能配置任务:主要是对一些全局相关的业务功能的前置配置,例如对首页业务数据缓存的预加载等,移除它们会造成业务有损。 这里可以使用业务降级或者业务打散的方式进行处理,如一个网络请求请求包含一些基础数据,还包含一些图片数据,可以将这个网络请求分成两个接口,优先去获取
基础数据,再去获取图片数据,图片数据又可以分为前台可见图片和后台不可见图片,这些都可以排列先后顺序进行处理,尽量让首页优先展示。全局配置任务:主要是对于全局 UI 配置,文件路径的处理操作,它们占比少,耗时少,是首页创建的前置任务,优化方面可以暂不处理。
1.线程优化基础方案:
严禁使用new Thread的方式创建对象,这种方式创建的对象,如果线程一直没处理完或者处理缓慢,最直观的感受就是界面卡顿。推荐使用线程池的方式处理。 提供基础线程池供各个业务使用,不要让各个业务维护各自的线程池,防止线程过多,占用过多CPU时间。 根据任务类型选择合适的异步方式:优先级低,长时间执行,HandlerThread;定时执行耗时任务,线程池。 创建线程必须命名,以方便定位线程归属,在运行期 Thread.currentThread().setName 修改名字。 关键异步任务监控,注意异步不等于不耗时,建议使用AOP的方式来做监控。 重视优先级设置(根据任务具体情况),Process.setThreadPriority() 可以设置多次。
2.线程收敛
DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
@Override protected void afterHookedMethod(MethodHookParam param)throws Throwable {
super.afterHookedMethod(param);
Thread thread = (Thread) param.thisObject;
LogUtils.i("stack " + Log.getStackTraceString(new Throwable());
}
);
2.线程收敛:根据线程创建堆栈考量合理性,使用统一线程库。那么如何统一线程库?
IO密集型任务:IO密集型任务不消耗CPU,核心池可以很大。常见的IO密集型任务如文件读取、写入,网络请求等等。 CPU密集型任务:核心池大小和CPU核心数相关。常见的CPU密集型任务如比较复杂的计算操作,此时需要使用大量的CPU计算单元。
public class DispatcherExecutor {
/**
* CPU 密集型任务的线程池
*/
private static ThreadPoolExecutor sCPUThreadPoolExecutor;
/**
* IO 密集型任务的线程池
*/
private static ExecutorService sIOThreadPoolExecutor;
/**
* 当前设备可以使用的 CPU 核数
*/
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
/**
* 线程池核心线程数,其数量在2 ~ 5这个区域内
*/
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 5));
/**
* 线程池线程数的最大值:这里指定为了核心线程数的大小
*/
private static final int MAXIMUM_POOL_SIZE = CORE_POOL_SIZE;
/**
* 线程池中空闲线程等待工作的超时时间,当线程池中
* 线程数量大于corePoolSize(核心线程数量)或
* 设置了allowCoreThreadTimeOut(是否允许空闲核心线程超时)时,
* 线程会根据keepAliveTime的值进行活性检查,一旦超时便销毁线程。
* 否则,线程会永远等待新的工作。
*/
private static final int KEEP_ALIVE_SECONDS = 5;
/**
* 创建一个基于链表节点的阻塞队列
*/
private static final BlockingQueue<Runnable> S_POOL_WORK_QUEUE = new LinkedBlockingQueue<>();
/**
* 用于创建线程的线程工厂
*/
private static final DefaultThreadFactory S_THREAD_FACTORY = new DefaultThreadFactory();
/**
* 线程池执行耗时任务时发生异常所需要做的拒绝执行处理
* 注意:一般不会执行到这里
*/
private static final RejectedExecutionHandler S_HANDLER = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
Executors.newCachedThreadPool().execute(r);
}
};
/**
* 获取CPU线程池
*
* @return CPU线程池
*/
public static ThreadPoolExecutor getCPUExecutor() {
return sCPUThreadPoolExecutor;
}
/**
* 获取IO线程池
*
* @return IO线程池
*/
public static ExecutorService getIOExecutor() {
return sIOThreadPoolExecutor;
}
/**
* 实现一个默认的线程工厂
*/
private static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "TaskDispatcherPool-" +
POOL_NUMBER.getAndIncrement() +
"-Thread-";
}
@Override
public Thread newThread(Runnable r) {
// 每一个新创建的线程都会分配到线程组group当中
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon()) {
// 非守护线程
t.setDaemon(false);
}
// 设置线程优先级
if (t.getPriority() != Thread.NORM_PRIORITY) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
}
static {
sCPUThreadPoolExecutor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
S_POOL_WORK_QUEUE, S_THREAD_FACTORY, S_HANDLER);
// 设置是否允许空闲核心线程超时时,线程会根据keepAliveTime的值进行活性检查,一旦超时便销毁线程。否则,线程会永远等待新的工作。
sCPUThreadPoolExecutor.allowCoreThreadTimeOut(true);
// IO密集型任务线程池直接采用CachedThreadPool来实现,
// 它最多可以分配Integer.MAX_VALUE个非核心线程用来执行任务
sIOThreadPoolExecutor = Executors.newCachedThreadPool(S_THREAD_FACTORY);
}
}
// 如果当前执行的任务是CPU密集型任务,则从基础线程池组件
// DispatcherExecutor中获取到用于执行 CPU 密集型任务的线程池
DispatcherExecutor.getCPUExecutor().execute(YourRunable());
// 如果当前执行的任务是IO密集型任务,则从基础线程池组件
// DispatcherExecutor中获取到用于执行 IO 密集型任务的线程池
DispatcherExecutor.getIOExecutor().execute(YourRunable());
3.异步启动器:
启动器流程:
启动器的主题流程主要分为主线程与并发两个区域块。而 head task与tail task 仅仅是用于处理启动前/启动后的一些通用任务,例如我们可以在head task中做一些获取通用信息的操作,在tail task可以做一些log输出、数据上报等操作。
// 1.初始化任务分发器
TaskDispatcher.init(this)
// 2.添加并开始执行初始化任务
val dispatcher = TaskDispatcher.createInstance()
//3.给启动器添加任务
dispatcher
.addTask(InitAMapTask()) // 高德 SDK 初始化任务
.addTask(InitStethoTask()) // Stetho 初始化任务
.addTask(InitWeexTask()) // weex 初始化任务
.addTask(InitBuglyTask()) // Bugly 初始化任务
.addTask(InitFrescoTask()) // Frescode 初始化任务
.addTask(InitJPushTask()) // 极光推送 SDK 初始化任务
.addTask(InitUmengTask()) // 友盟 SDK 初始化任务
.addTask(GetDeviceIdTask()) // 获取设备 ID 初始化任务
.addTask(DelayInitTaskA()) // 延迟初始化任务 A
.addTask(DelayInitTaskB()) // 延迟初始化任务 B
.start()
//4.等待需要wait的任务执行完毕才继续向下执行
dispatcher.await()
fun init(context: Context) {
Companion.context = context
sHasInit = true
isMainProcess = Utils.isMainProcess(Companion.context)
}
/**
* 注意:每次获取的都是新对象
*/
@JvmStatic
fun createInstance(): TaskDispatcher {
if (!sHasInit) {
throw RuntimeException("must call TaskDispatcher.init first")
}
return TaskDispatcher()
}
// 任务接口
interface ITask {
/**
* Task主任务执行完成之后需要执行的任务
*/
fun getTailRunnable(): Runnable?
fun setTaskCallBack(callBack: TaskCallBack)
fun needCall(): Boolean
/**
* 优先级的范围,可根据Task重要程度及工作量指定;之后根据实际情况决定是否有必要放更大
*/
@IntRange(
from = Process.THREAD_PRIORITY_FOREGROUND.toLong(),
to = Process.THREAD_PRIORITY_LOWEST.toLong()
)
fun priority(): Int
fun run()
/**
* Task执行所在的线程池,可指定,一般默认
*/
fun runOn(): Executor?
/**
* 依赖关系
*/
fun dependsOn(): List<Class<out Task?>?>?
/**
* 异步线程执行的Task是否需要在被调用await的时候等待,默认不需要
*/
fun needWait(): Boolean
/**
* 是否在主线程执行
*/
fun runOnMainThread(): Boolean
/**
* 只是在主进程执行
*/
fun onlyInMainProcess(): Boolean
}
接口使用runOnMainThread表示是否是主线程任务还是异步线程任务。 接口使用dependsOn来增加依赖关系。
/**
* 需要在getDeviceId之后执行
*/
class InitJPushTask : Task() {
override fun dependsOn(): List<Class<out Task?>>? {
val task: MutableList<Class<out Task?>> = ArrayList()
task.add(GetDeviceIdTask::class.java)
return task
}
override fun run() {
JPushInterface.init(mContext)
val app = mContext as MyApplication
JPushInterface.setAlias(mContext, 0, app.deviceId)
}
}
override fun run() {
//给应用打点监控每个任务的耗时
Trace.beginSection(mTask.javaClass.simpleName)
//设置线程优先级
Process.setThreadPriority(mTask.priority())
//等待依赖任务执行完毕,内部会一直await住
mTask.waitToSatisfy()
// 执行Task
mTask.isRunning = true
mTask.run()
// 执行Task的尾部任务
val tailRunnable = mTask.getTailRunnable()
tailRunnable?.run()
//执行完毕后标记该任务已完成并将依赖他的任务的countdown -1;
if (!mTask.needCall() || !mTask.runOnMainThread()) {
printTaskLog(startTime, waitTime)
TaskStat.markTaskDone()
mTask.isFinished = true
mTaskDispatcher.satisfyChildren(mTask)
mTaskDispatcher.markTaskDone(mTask)
DispatcherLog.i(mTask.javaClass.simpleName + " finish")
}
Trace.endSection()
}
看satisfyChildren和markTaskDone
/**
* 通知Children一个前置任务已完成
*/
fun satisfyChildren(launchTask: Task) {
val arrayList = mDependedHashMap[launchTask.javaClass]
if (arrayList != null && arrayList.size > 0) {
for (task in arrayList) {
task.satisfy()
}
}
}
//改变当前task在架构中给的状态:如添加到执行完成队列,删除在mNeedWaitTasks的task,将等待mCountDownLatch,mNeedWaitCount计数器值减1.
fun markTaskDone(task: Task) {
if (needWait(task)) {
mFinishedTasks.add(task.javaClass)
mNeedWaitTasks.remove(task)
mCountDownLatch!!.countDown()
mNeedWaitCount.getAndDecrement()
}
}
@UiThread
fun await() {
try {
...
if (mNeedWaitCount.get() > 0) {
if (mCountDownLatch == null) {
throw RuntimeException("You have to call start() before call await()")
}
// 等待 10 秒
mCountDownLatch?.await(WAIT_TIME.toLong(), TimeUnit.MILLISECONDS)
}
} catch (e: InterruptedException) {
}
}
/**
* 需要等待的任务数
*/
private val mNeedWaitCount = AtomicInteger() //
可以看到异步启动器的核心是一定要了解任务的依赖关系,对任务属性的需要有一定深刻了解。
mAllTasks = TaskSortUtil.getSortResult(mAllTasks, mClsAllTasks)
/**
* 任务的有向无环图的拓扑排序
*/
@Synchronized
fun getSortResult(
originTasks: List<Task>,
clsLaunchTasks: List<Class<out Task>>
): MutableList<Task> {
val makeTime = System.currentTimeMillis()
val dependSet: MutableSet<Int> = ArraySet()
val graph = Graph(originTasks.size)
for (i in originTasks.indices) {
val task = originTasks[i]
val list = task.dependsOn()
if (task.isSend || list == null || list.isEmpty()) {
continue
}
for (cls in task.dependsOn()!!) {
val indexOfDepend = getIndexOfTask(originTasks, clsLaunchTasks, cls)
check(indexOfDepend >= 0) {
task.javaClass.simpleName +
" depends on " + cls.simpleName + " can not be found in task list "
}
dependSet.add(indexOfDepend)
graph.addEdge(indexOfDepend, i)
}
}
val indexList: List<Int> = graph.topologicalSort()
val newTasksAll = getResultTasks(originTasks, dependSet, indexList)
DispatcherLog.i("task analyse cost makeTime " + (System.currentTimeMillis() - makeTime))
printAllTaskName(newTasksAll)
return newTasksAll
}
4.延迟初始化:
/**
* 延迟初始化分发器
*/
public class DelayInitDispatcher {
private Queue<Task> mDelayTasks = new LinkedList<>();
private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
@Override
public boolean queueIdle() {
// 分批执行的好处在于每一个task占用主线程的时间相对
// 来说很短暂,并且此时CPU是空闲的,这些能更有效地避免UI卡顿
if(mDelayTasks.size()>0){
Task task = mDelayTasks.poll();
new DispatchRunnable(task).run();
}
return !mDelayTasks.isEmpty();
}
};
public DelayInitDispatcher addTask(Task task){
mDelayTasks.add(task);
return this;
}
public void start(){
Looper.myQueue().addIdleHandler(mIdleHandler);
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
GlobalHandler.getInstance().getHandler().post((Runnable) () -> {
if (hasFocus) {
DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
delayInitDispatcher.addTask(new InitOtherTask())
.start();
}
});
}
4.1.4:Activity阶段优化
1.Splash 与 Main 合并
进入 SplashActivity,在 SplashActivity 中判断当前是否有待展示的开屏; 如果有待展示的开屏则展示开屏,等待开屏展示结束再跳转到 MainActivity,如果没有开屏则直接跳转到 MainActivity。
合并后收益:
合并前需要经过两次Activity的启动,合并后只有一次,减少一次启动时间。 利用读取开屏信息的时间,做一些与Activity强关联的并发任务,比如异步View预加载等。
合并后如何解决外部通过 Activity 名称跳转的问题; 如何解决 LaunchMode 与多实例的问题。
接下来我们来看一下第二个问题。
多实例问题:
内部启动多实例的问题
为了尽可能少的侵入业务,同时也防止后续迭代再出现内部启动导致 MainActivity 问题,我们对 Context startActivity 的调用进行了插桩。对于启动 MainActivity 的调用,在完成向 Intent 中添加 flag 和替换 Component 信息后再调用原有实现。之所以选择插桩方式实现,是因为抖音的代码结构比较复杂,存在多个基类 Activity,且部分基类 Activity 无法直接修改到代码。对于没有这方面问题的业务,可以通过重写基类 Activtity 及 Application 的 startActivity 方法的方式实现。
外部启动多实例问题
具体来说我们的优化方案如下:
需要注意的是我们这里 hook Instrumentaion 的实现方案,在高版本的 Android 系统上我们也可以以 AppComponentFactory instantiateActivity 的方式替换。
2.xml2code
这里优化主要讲的是第二种xml文件配置,一般流程:
将 xml 文件解析到内存中 XmlResourceParser 的 IO 过程; 根据 XmlResourceParser 的 Tag name 获取 Class 的 Java 反射过程; 创建 View 实例,最终生成 View 树。
传统优化方式:
优化xml布局层级,使用ConstraintLayout平滑布局非嵌套布局。 使用ViewStub实现按需加载。
我们知道LayoutParams参数在inflate阶段会根据root属性进行设置,如果root为空那么LayoutParams就会为空,这个时候被添加到View中就会使用默认值,那么自己设置的那些View的属性就会丢失。
这显然不符合预期情况,解决这个问题,需要在进行预加载时候 new 一个相应类型的 root,以实现对待 inflate view 属性的正确解析。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
// 省略其他逻辑
if (root != null) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
root.setLayoutParams(params);
}
}
}
public void addView(View child, int index) {
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
4.2:全局优化
1.GC抑制
大家都知道GC的过程会有CPU从应用层到内核层的一个转变,这个过程:阻塞 Java 程序的执行,占用 CPU 资源,用设计者说的话:世界都停止了。所以在启动阶段对Gc的优化可以大大改善界面的流畅度和启动速度。
gc触发条件之一就是内存达到了某个阈值,所以我们的方案就是减少内存的申请和占用,所以解决gc问题就需要改造我们的代码,尽量减少代码的执行量和对象的创建特别是一些高内存图片。还可以使用的是使用一些特殊手段达到对某些Gc类型的抑制和可控。
参考这篇文章:支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」。
https://juejin.cn/post/6844903705028853767#heading-3
2.类重排
3.类加载优化
4.资源重排
利用 Linux 的 IO 读取策略,PageCache 和 ReadAhead 机制,按照读取顺序重新排列,减少磁盘 IO 次数 。具体操作可以参考支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能 这篇文章。
https://juejin.cn/post/6844903710020091917
5.应用启动耗时防劣化。
而对于一些中小型项目就只能在开发过程中注意咯,你觉得呢?
抖音 Android 性能优化系列:启动优化实践
https://juejin.cn/post/7080065015197204511#heading-4
抖音BoostMultiDex优化实践:Android低版本上APP首次启动时间减少80%(一)
https://mp.weixin.qq.com/s/jINCbTJ5qMaD6NdeGBHEwQ
深入探索Android启动速度优化(上)
https://juejin.cn/post/6844904093786308622#heading-133
老大难的GC原理及调优,这下全说清楚了
https://juejin.cn/post/6844903953004494856
抖音 Android 性能优化系列:启动优化之理论和工具篇
https://juejin.cn/post/7058080006022856735
心遇 Android 启动优化实践:将启动时间降低 50%
https://juejin.cn/post/7138596300672958471
Android App 启动优化全记录
https://juejin.cn/post/7109655666683281415
抖音 Android 性能优化系列:新一代全能型性能分析工具 Rhea
https://mp.weixin.qq.com/s/vkBeZ6hmVn_RaXS5Xv_L2g
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!